package com.wh1200.mybatis.xmlless.plugin

import com.wh1200.mybatis.xmlless.annotations.UseEntityConvert
import com.wh1200.mybatis.xmlless.plugin.visitor.ColumnReplaceVisitor
import com.wh1200.mybatis.xmlless.plugin.visitor.SelectVisitor
import com.wh1200.mybatis.xmlless.util.XmllessInfoUtil
import net.sf.jsqlparser.parser.CCJSqlParserUtil
import org.apache.ibatis.builder.StaticSqlSource
import org.apache.ibatis.cache.CacheKey
import org.apache.ibatis.executor.Executor
import org.apache.ibatis.mapping.BoundSql
import org.apache.ibatis.mapping.MappedStatement
import org.apache.ibatis.mapping.ParameterMapping
import org.apache.ibatis.mapping.SqlCommandType
import org.apache.ibatis.plugin.Interceptor
import org.apache.ibatis.plugin.Intercepts
import org.apache.ibatis.plugin.Invocation
import org.apache.ibatis.plugin.Signature
import org.apache.ibatis.reflection.MetaObject
import org.apache.ibatis.reflection.SystemMetaObject
import org.apache.ibatis.session.ResultHandler
import org.apache.ibatis.session.RowBounds
import org.slf4j.LoggerFactory


/**
 *
 * @author 吴昊
 * @date 2023/12/13 17:02
 * @since v4.0.0
 * @version 1.0
 */
@Intercepts(
    Signature(
        type = Executor::class, method = "query",
        args = [
          MappedStatement::class, Any::class, RowBounds::class, ResultHandler::class
        ]
    ),
    Signature(
        type = Executor::class, method = "query",
        args = [
          MappedStatement::class, Any::class, RowBounds::class, ResultHandler::class,
          CacheKey::class, BoundSql::class
        ]
    ),
    Signature(
        type = Executor::class, method = "update",
        args = [
          MappedStatement::class, Any::class
        ]
    )
)
class SqlConvertInterceptor : Interceptor {

  private val logger = LoggerFactory.getLogger(SqlConvertInterceptor::class.java)

  override fun intercept(invocation: Invocation): Any? {
    val target = invocation.target
    if (target !is Executor) {
      return invocation.proceed()
    }
    val ms = invocation.args[0] as MappedStatement
    if (!isEntityMapMethod(ms)) {
      try {
        return invocation.proceed()
      } catch (e: Exception) {
        val obj = invocation.args[1]
        val boundSql = ms.getBoundSql(obj)
        logger.error("执行sql出错，sql：${boundSql.sql}", e)
        throw e
      }
    }
    val obj = invocation.args[1]
    val boundSql = ms.getBoundSql(obj)
    val statement = CCJSqlParserUtil.parse(boundSql.sql)
    val visitor = SelectVisitor()
    statement.accept(visitor)
    statement.accept(
        ColumnReplaceVisitor(visitor.getTables())
    )
    val isUpdate = invocation.args.size == 2
    if (!isUpdate && ms.sqlCommandType == SqlCommandType.SELECT) {
      val meta = MetaObject.forObject(
          boundSql,
          SystemMetaObject.DEFAULT_OBJECT_FACTORY,
          SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,
          CurrentUserInterceptor.DEFAULT_REFLECTOR_FACTORY
      )
      meta.setValue("sql", statement.toString())
      val parameter = invocation.args[1]
      val rowBounds = invocation.args[2] as RowBounds?
      val resultHandler = invocation.args[3] as ResultHandler<*>?
      val cacheKey: CacheKey = target.createCacheKey(ms, parameter, rowBounds, boundSql)
      return target.query<Any>(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql)
    }
    invocation.args[0] = toNewStatement(ms, statement.toString(), boundSql.parameterMappings)
    return invocation.proceed()
  }


  private fun isEntityMapMethod(ms: MappedStatement): Boolean {
    if (XmllessInfoUtil.isMapToEntityMethod(ms)) {
      return true
    }
    val mapper = ms.configuration.mapperRegistry.mappers.first {
      ms.id.startsWith(it.name + ".")
    }
    val methodName = ms.id.substring(ms.id.lastIndexOf(".") + 1).replace("_mpCount", "")
    val method = mapper.methods.find { it.name == methodName } ?: return false
    if (method.isAnnotationPresent(UseEntityConvert::class.java)) {
      XmllessInfoUtil.addMapToEntityMethod(ms.id, mapper, method)
      return true
    }
    return false
  }

}


/**
 * 替换sql，生成新的MappedStatement对象
 *
 * @param ms
 * @param newSql
 * @param parameterMappings
 * @return
 */
internal fun toNewStatement(
    ms: MappedStatement,
    newSql: String,
    parameterMappings: MutableList<ParameterMapping>,
): MappedStatement {
  val newSqlSource = StaticSqlSource(
      ms.configuration, newSql, parameterMappings
  )
  val newMs = MappedStatement.Builder(
      ms.configuration,
      ms.id,
      newSqlSource,
      ms.sqlCommandType
  ).resource(ms.resource)
      .fetchSize(ms.fetchSize)
      .statementType(ms.statementType)
      .keyGenerator(ms.keyGenerator)
      .timeout(ms.timeout)
      .parameterMap(ms.parameterMap)
      .resultMaps(ms.resultMaps)
      .resultSetType(ms.resultSetType)
      .cache(ms.cache)
      .flushCacheRequired(ms.isFlushCacheRequired)
      .useCache(ms.isUseCache)
  if (ms.keyProperties != null && ms.keyProperties.isNotEmpty()) {
    newMs.keyProperty(ms.keyProperties.joinToString(","))
  }
  if (ms.keyColumns != null && ms.keyColumns.isNotEmpty()) {
    newMs.keyColumn(ms.keyColumns.joinToString(","))
  }
  return newMs.build()
}
